package org.zgstudio.ddlchecker;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.io.output.FileWriterWithEncoding;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.xml.sax.SAXException;

import java.awt.Component;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

interface GUIPostDelListener {
    void delAPostAction(Link link, boolean succ);
}

class GUIDeliciousPostsMgrV1 extends DeliciousPostsMgrV1 implements GUIPostDelListener {
    DDLCheckerGUI gui;
    
    GUIDeliciousPostsMgrV1(DDLCheckerGUI gui) { this.gui = gui; }
    
    public boolean delPost (String url2del) throws IOException, SAXException, ParserConfigurationException {
        boolean succ = super.delPost(url2del);
        Link link = new Link(new URL(url2del));
        delAPostAction(link, succ);
        gui.getCacheMgr().clearAll();
        return succ;
    }
    
    @Override
    public void delAPostAction(Link link, boolean succ) {
        JTable tbl = (JTable)gui.getComponent("invalid-links-table");
        LinksTableModel model = (LinksTableModel)tbl.getModel();
        LinkCheckResult result = new LinkCheckResult();
        result.setLinkChecked(link);
        if (succ)
            result.setResultCode(ResultCode.DELETED);
        else
            result.setResultCode(ResultCode.DELETE_FSILED);
        result.setCheckTime(new Date());
        result.setCheckTime(new Date());
        model.addLinkCheckResult(result);
    }
}

class CacheManager {
    private boolean disabled = false;
    private Map<String, Object> cache = new HashMap<String, Object>();

    public boolean isDisabled() {
        return disabled;
    }

    public void setDisabled(boolean disabled) {
        this.disabled = disabled;
    }
    
    public Object getCache(String key) {
        return cache.get(key);
    }
    
    public void setCache(String key, Object value) {
        cache.put(key, value);
    }
    
    public void clearAll() {
        cache.clear();
    }
}

class DDLCacheManager extends CacheManager {
    private static String KEY_RECENT_POSTS = "recent-posts";
    private static String KEY_ALL_POSTS = "all-posts";
    private static String KEY_RECENT_POSTS_INVALID = "invalid-recent-posts";
    private static String KEY_ALL_POSTS_INVALID    = "invalid-all-posts";
    private static String KEY_RECENT_POSTS_CHECKRESULT = "recent-posts-checkresult";
    private static String KEY_ALL_POSTS_CHECKRESULT = "all-posts-checkresult";
    private static String FILE_SUFFIX = "log";
    
    public void clearAll() {
        super.clearAll();
        File file = null;
        
        file = new File(KEY_RECENT_POSTS+"."+FILE_SUFFIX);
        if (file.exists() && file.isFile()) file.delete();
        
        file = new File(KEY_ALL_POSTS+"."+FILE_SUFFIX);
        if (file.exists() && file.isFile()) file.delete();
        
        file = new File(KEY_RECENT_POSTS_INVALID+"."+FILE_SUFFIX);
        if (file.exists() && file.isFile()) file.delete();
        
        file = new File(KEY_ALL_POSTS_INVALID+"."+FILE_SUFFIX);
        if (file.exists() && file.isFile()) file.delete();
        
        file = new File(KEY_RECENT_POSTS_CHECKRESULT+"."+FILE_SUFFIX);
        if (file.exists() && file.isFile()) file.delete();
        
        file = new File(KEY_ALL_POSTS_CHECKRESULT+"."+FILE_SUFFIX);
        if (file.exists() && file.isFile()) file.delete();
    }
    
    private void writeLinks2File(String fileName, Collection<Link> links) throws IOException {
        FileWriterWithEncoding fw = new FileWriterWithEncoding(fileName, "UTF-8");
        for (Iterator<Link> it = links.iterator(); it.hasNext();) {
            Link link = it.next();
            IOUtils.write(link.getUrl().toString()+IOUtils.LINE_SEPARATOR, fw);
            IOUtils.write(link.getProperty("description")+IOUtils.LINE_SEPARATOR, fw);
            String dateStr = DeliciousPostSAXHandler.df.format((Date)link.getProperty("addTime"));
            IOUtils.write(dateStr+IOUtils.LINE_SEPARATOR, fw);
        }
        fw.close();
    }
    
    private Collection<Link> readLinksFromFile(String fileName) throws IOException, ParseException {
        List<Link> links = new ArrayList<Link>();
        File file = new File(fileName);
        if (!file.exists()) return null;
        LineIterator lineIt = FileUtils.lineIterator(file, "UTF-8");
        while (lineIt.hasNext()){
            String line = lineIt.nextLine();
            Link link = null;
            if (line != null && line.trim().length() > 0) {
                link = new Link(new URL(line.trim()));
                links.add(link);
            }
            
            line = lineIt.nextLine();
            link.setProperty("description", line);
            
            line = lineIt.nextLine();
            Date date = DeliciousPostSAXHandler.df.parse(line);
            link.setProperty("addTime", date);
        }
        return links;
    }
    
    private void writeLCR2File(String fileName, Collection<LinkCheckResult> lcrs) throws IOException  {
        FileWriterWithEncoding fw = new FileWriterWithEncoding(fileName, "UTF-8");
        for (Iterator<LinkCheckResult> it = lcrs.iterator(); it.hasNext();) {
            LinkCheckResult lcr = it.next();
            IOUtils.write(lcr.getLinkChecked().getUrl().toString(), fw);
        }
        fw.close();
    }
    
    public void setRecentPosts (Collection<Link> links) {
        setCache(KEY_RECENT_POSTS, links);
        try {
            writeLinks2File(KEY_RECENT_POSTS+"."+FILE_SUFFIX, links);
        } catch (IOException e) {
            System.err.println("Cannot write links to file " +
                    KEY_RECENT_POSTS+"."+FILE_SUFFIX);
            e.printStackTrace();
        }
    }
    
    @SuppressWarnings("unchecked")
    public Collection<Link> getRecentPosts () {
        if (getCache(KEY_RECENT_POSTS) != null)
            return (Collection<Link>)getCache(KEY_RECENT_POSTS);
        else {
            try {
                Collection<Link> links = readLinksFromFile(KEY_RECENT_POSTS+"."+FILE_SUFFIX);
                if (links != null) {
                    setCache(KEY_RECENT_POSTS, links);
                    return links;
                }
            } catch (Exception e) {
                System.err.println("Cannot read links from file " +
                        KEY_RECENT_POSTS+"."+FILE_SUFFIX);
            }
            return null;
        }
    }
    
    public void setRecentInvalidPosts (Collection<Link> links) {
        setCache(KEY_RECENT_POSTS_INVALID, links);
        try {
            writeLinks2File(KEY_RECENT_POSTS_INVALID+"."+FILE_SUFFIX, links);
        } catch (IOException e) {
            System.err.println("Cannot write links to file " +
                    KEY_RECENT_POSTS_INVALID+"."+FILE_SUFFIX);
            e.printStackTrace();
        }
    }
    
    @SuppressWarnings("unchecked")
    public Collection<Link> getRecentInvalidPosts () {
        if (getCache(KEY_RECENT_POSTS_INVALID) != null)
            return (Collection<Link>)getCache(KEY_RECENT_POSTS_INVALID);
        else {
            try {
                Collection<Link> links = readLinksFromFile(KEY_RECENT_POSTS_INVALID+"."+FILE_SUFFIX);
                if (links != null) {
                    setCache(KEY_RECENT_POSTS_INVALID, links);
                    return links;
                }
            } catch (Exception e) {
                System.err.println("Cannot read links from file " +
                        KEY_RECENT_POSTS_INVALID+"."+FILE_SUFFIX);
            }
            return null;
        }
    }
    
    public void setAllPosts (Collection<Link> links) {
        setCache(KEY_ALL_POSTS, links);
        try {
            writeLinks2File(KEY_ALL_POSTS+"."+FILE_SUFFIX, links);
        } catch (IOException e) {
            System.err.println("Cannot write links to file " +
                    KEY_ALL_POSTS+"."+FILE_SUFFIX);
            e.printStackTrace();
        }
    }
    
    @SuppressWarnings("unchecked")
    public Collection<Link> getAllPosts () {
        if (getCache(KEY_ALL_POSTS) != null)
            return (Collection<Link>)getCache(KEY_ALL_POSTS);
        else {
            try {
                Collection<Link> links = readLinksFromFile(KEY_ALL_POSTS+"."+FILE_SUFFIX);
                if (links != null) {
                    setCache(KEY_ALL_POSTS, links);
                    return links;
                }
            } catch (Exception e) {
                System.err.println("Cannot read links from file " +
                        KEY_ALL_POSTS+"."+FILE_SUFFIX);
            }
            return null;
        }
    }
    
    public void setAllInvalidPosts (Collection<Link> links) {
        setCache(KEY_ALL_POSTS_INVALID, links);
        try {
            writeLinks2File(KEY_ALL_POSTS_INVALID+"."+FILE_SUFFIX, links);
        } catch (IOException e) {
            System.err.println("Cannot write links to file " +
                    KEY_ALL_POSTS_INVALID+"."+FILE_SUFFIX);
            e.printStackTrace();
        }
    }
    
    @SuppressWarnings("unchecked")
    public Collection<Link> getAllInvalidPosts () {
        if (getCache(KEY_ALL_POSTS_INVALID) != null)
            return (Collection<Link>)getCache(KEY_ALL_POSTS_INVALID);
        else {
            try {
                Collection<Link> links = readLinksFromFile(KEY_ALL_POSTS_INVALID+"."+FILE_SUFFIX);
                if (links != null) {
                    setCache(KEY_ALL_POSTS_INVALID, links);
                    return links;
                }
            } catch (Exception e) {
                System.err.println("Cannot read links from file " +
                        KEY_ALL_POSTS_INVALID+"."+FILE_SUFFIX);
            }
            return null;
        }
    }
    
    /************ Unused **********************/
    public void setRecentPostsCR (Collection<LinkCheckResult> linkcr) {
        setCache(KEY_RECENT_POSTS_CHECKRESULT, linkcr);
    }
    
    @SuppressWarnings("unchecked")
    public Collection<LinkCheckResult> getRecentPostsCR () {
        return (Collection<LinkCheckResult>)getCache(KEY_RECENT_POSTS_CHECKRESULT);
    }
    public void setAllPostsCR (Collection<LinkCheckResult> linkcr) {
        setCache(KEY_ALL_POSTS_CHECKRESULT, linkcr);
    }
    
    @SuppressWarnings("unchecked")
    public Collection<LinkCheckResult> getAllPostsCR () {
        return (Collection<LinkCheckResult>)getCache(KEY_ALL_POSTS_CHECKRESULT);
    }
}

/**
 * GUI for Delicious Dead Link Checker
 * 
 * @author zhguo
 * 
 */
public class DDLCheckerGUI {

    private Map<String, Component> componentMaps = new HashMap<String, Component>();
    private DeliciousPostsMgrV1    delMgr;
    private DDLCacheManager cacheMgr = new DDLCacheManager(); 

    public DDLCacheManager getCacheMgr() {
        return cacheMgr;
    }

    public void setCacheMgr(DDLCacheManager cacheMgr) {
        this.cacheMgr = cacheMgr;
    }

    public DDLCheckerGUI() {
        delMgr = new GUIDeliciousPostsMgrV1(this);
    }
    
    private void addComponent(String id, Component comp) {
        componentMaps.put(id, comp);
    }

    public Component getComponent(String id) {
        return componentMaps.get(id);
    }

    public void createAndShowGUI() {
        JFrame frame = new JFrame("Delicious Dead Link Checker");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        DDLCheckerGUIListener guiListener = new DDLCheckerGUIListener(this);

        final JTable linksTable = new JTable();
        linksTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        linksTable.setFillsViewportHeight(true);
        linksTable.setModel(new LinksTableModel());
        linksTable.setDefaultRenderer(Date.class, new LinkJLabelCR());
        linksTable.setDefaultRenderer(String.class, new LinkJLabelCR());
        addComponent("links-table", linksTable);

        final JTable validLinksTable = new JTable();
        validLinksTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        validLinksTable.setFillsViewportHeight(true);
        validLinksTable.setModel(new LinksTableModel());
        addComponent("valid-links-table", validLinksTable);
        
        final JTable invalidLinksTable = new JTable();
        invalidLinksTable.setFillsViewportHeight(true);
        invalidLinksTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        invalidLinksTable.setModel(new LinksTableModel());
        addComponent("invalid-links-table", invalidLinksTable);
        
        Dimension size = new Dimension();

        JTabbedPane listTabPanel = new JTabbedPane();
        listTabPanel.addTab("Bookmarks", new JScrollPane(linksTable));
        listTabPanel.addTab("valid bookmarks", new JScrollPane(validLinksTable));
        listTabPanel.addTab("invalid bookmarks",
                new JScrollPane(invalidLinksTable));
        listTabPanel.setPreferredSize(new Dimension(200, 200));

        JScrollPane listScrollPanel = new JScrollPane(listTabPanel);

        final JTextPane detailedInfoScrollPanel = new JTextPane();
        detailedInfoScrollPanel.setContentType("text/html");
        detailedInfoScrollPanel.setEditable(false);

        ListSelectionModel rowSM = linksTable.getSelectionModel();
        rowSM.addListSelectionListener(new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent e) {
                if (e.getValueIsAdjusting())
                    return;

                ListSelectionModel lsm = (ListSelectionModel) e.getSource();
                if (lsm.isSelectionEmpty()) {
                    System.out.println("No rows are selected.");
                } else {
                    int selectedRow = lsm.getMinSelectionIndex();
                    System.out.println("Row " + selectedRow
                            + " is now selected.");
                    StringBuilder sb = new StringBuilder("<html>");
                    for (int i = 0; i < linksTable.getModel().getColumnCount(); ++i) {
                        if (i == LinksTableModel.URL_INDEX - LinksTableModel.HIDDEN_COLUMN_COUNT) {
                            String url = (String)linksTable.getModel().getValueAt(selectedRow, i);
                            sb.append("<a href=\""+url+"\">"+url+"</a><br>");
                        } else
                            sb.append(linksTable.getModel().getValueAt(selectedRow,
                                i).toString() + "<br>");
                    }
                    sb.append("</html>");
                    detailedInfoScrollPanel.setText(sb.toString());
                }
            }
        });

        rowSM = validLinksTable.getSelectionModel();
        rowSM.addListSelectionListener(new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent e) {
                if (e.getValueIsAdjusting())
                    return;

                ListSelectionModel lsm = (ListSelectionModel) e.getSource();
                if (lsm.isSelectionEmpty()) {
                    System.out.println("No rows are selected.");
                } else {
                    int selectedRow = lsm.getMinSelectionIndex();
                    System.out.println("Row " + selectedRow
                            + " is now selected.");
                    StringBuilder sb = new StringBuilder("<html>");
                    for (int i = 0; i < validLinksTable.getModel().getColumnCount(); ++i) {
                        if (i == LinksTableModel.URL_INDEX - LinksTableModel.HIDDEN_COLUMN_COUNT) {
                            String url = (String)validLinksTable.getModel().getValueAt(selectedRow, i);
                            sb.append("<a href=\""+url+"\">"+url+"</a><br>");
                        } else
                            sb.append(linksTable.getModel().getValueAt(selectedRow,
                                i).toString() + "<br>");
                    }
                    sb.append("</html>");
                    detailedInfoScrollPanel.setText(sb.toString());
                }
            }
        });
        
        rowSM = invalidLinksTable.getSelectionModel();
        rowSM.addListSelectionListener(new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent e) {
                if (e.getValueIsAdjusting())
                    return;

                ListSelectionModel lsm = (ListSelectionModel) e.getSource();
                if (lsm.isSelectionEmpty()) {
                    System.out.println("No rows are selected.");
                } else {
                    int selectedRow = lsm.getMinSelectionIndex();
                    System.out.println("Row " + selectedRow
                            + " is now selected.");
                    StringBuilder sb = new StringBuilder("<html>");
                    for (int i = 0; i < invalidLinksTable.getModel().getColumnCount(); ++i) {
                        if (i == LinksTableModel.URL_INDEX - LinksTableModel.HIDDEN_COLUMN_COUNT) {
                            String url = (String)invalidLinksTable.getModel().getValueAt(selectedRow, i);
                            sb.append("<a href=\""+url+"\">"+url+"</a><br>");
                        } else
                            sb.append(linksTable.getModel().getValueAt(selectedRow,
                                i).toString() + "<br>");
                    }
                    sb.append("</html>");
                    detailedInfoScrollPanel.setText(sb.toString());
                }
            }
        });
        
        JPanel splitPanelWrapper = new JPanel();
        
        JSplitPane splitPanel = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
                listScrollPanel, new JScrollPane(detailedInfoScrollPanel));
        splitPanel.setOneTouchExpandable(true);
        splitPanelWrapper.add(splitPanel);
        splitPanel.setDividerLocation(305);

        int width = 800, height = 400;
        size.setSize(width, height);
        splitPanel.setPreferredSize(size);

        
        JPanel topPanel = new JPanel();
        
        // Login panel
        JPanel loginPanel = new JPanel();
        
        JPanel usernamePanel = new JPanel();
        usernamePanel.add(new JLabel("Username"));
        JTextField usernameField = new JTextField(10);
        addComponent("username-textfield", usernameField);
        usernamePanel.add(usernameField);
        
        JPanel passwordPanel = new JPanel();
        passwordPanel.add(new JLabel("Password"));
        JPasswordField passwordField = new JPasswordField(15);
        addComponent("password-textfield", passwordField);
        passwordPanel.add(passwordField);
        
        JButton loginBt = new JButton("Login");
        loginBt.addActionListener(new LoginBtActionListener(guiListener));

        loginPanel.add(usernamePanel);
        loginPanel.add(passwordPanel);
        loginPanel.add(loginBt);

        JProgressBar progressBar;
        progressBar = new JProgressBar();
        progressBar.setStringPainted(true);
        addComponent("progress-bar", progressBar);

        topPanel.add(loginPanel);
        topPanel.add(progressBar);
        
        // topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.X_AXIS));
        BoxLayout bl;
        // bl = new BoxLayout(loginPanel, BoxLayout.X_AXIS);
        // loginPanel.setLayout(bl);

        // action panel
        JPanel actionPanel = new JPanel();
        JButton loadBMRecentBt = new JButton("Load Recent Bookmarks");
        loadBMRecentBt.setActionCommand("load-recent-bookmarks");
        loadBMRecentBt.addActionListener(new BookmarksLoadRecentBtListener(guiListener));
        
        JButton loadBMAllBt = new JButton("Load All Bookmarks");
        loadBMAllBt.setActionCommand("load-all-bookmarks");
        loadBMAllBt.addActionListener(new BookmarksLoadAllBtListener(guiListener));

        JButton checkBt = new JButton("Check");
        checkBt.setActionCommand("check");
        
        final DDLCheckerGUI gui = this;
        final DeliciousPostsMgrV1 finalDelMgr = this.delMgr;
        ActionListener btListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if ("delete".equals(e.getActionCommand())) {
                    JTable invalidLinksTb = (JTable) gui.getComponent("invalid-links-table");
                    Collection<Link> links = ((LinksTableModel) invalidLinksTb.getModel()).getLinks();
                    List<String> urls = new ArrayList<String>();
                    for(Iterator<Link> it = links.iterator(); it.hasNext(); ) {
                        Link link = it.next();
                        urls.add(link.getUrl().toString());
                    }
                    
                    try {
                        JLabel statusLabel = (JLabel)gui.getComponent("status-label");
                        statusLabel.setText("Deletion started");
                        finalDelMgr.delPosts(urls.toArray(new String[0]));
                        statusLabel.setText("Deletion completed");
                    } catch (IOException e1) {
                        e1.printStackTrace();
                    } catch (SAXException e1) {
                        e1.printStackTrace();
                    } catch (ParserConfigurationException e1) {
                        e1.printStackTrace();
                    }
                } else if ("check".equals(e.getActionCommand())) {
                    new Thread() {
                        public void run() {
                            // DDLCacheManager cacheMgr = gui.getCacheMgr();
                            Collection<Link> links = null;
                            
                            /*
                            if (!cacheMgr.isDisabled()) {
                                links = cacheMgr.getAllInvalidPosts();
                            }
                            */
                            
                            ResultChecker rc = ResultChecker.getInstance();
                            rc.registerListener(new GUILinkCheckListener(gui));
                            SimpleLinkCheck lc = new SimpleLinkCheck(rc);
                            
                            JTable tbl = (JTable)gui.getComponent("links-table");
                            LinksTableModel model = (LinksTableModel)tbl.getModel();
                            links = model.getLinks();
                            try {
                                lc.checkLinks(links.toArray(new Link[] {}));
                            } catch (TooManyURLsInTaskException e1) {
                                e1.printStackTrace();
                            }
                        }
                    }.start();
                    System.out.println("started");
                }
            }
        };

        checkBt.addActionListener(btListener);

        JButton deleteBt = new JButton("Delete invalid bookmarks");
        deleteBt.setActionCommand("delete");
        deleteBt.addActionListener(btListener);

        actionPanel.add(loadBMRecentBt);
        actionPanel.add(loadBMAllBt);
        actionPanel.add(checkBt);
        actionPanel.add(deleteBt);

        // Status panel
        JScrollPane statusPanel = new JScrollPane();
        JLabel statusLabel = new JLabel();
        statusLabel.setText("Status");
        statusPanel.setViewportView(statusLabel);

        addComponent("status-label", statusLabel);

        // main panel
        JPanel mainPanel = new JPanel();
        mainPanel.add(topPanel);
        mainPanel.add(splitPanelWrapper);
        mainPanel.add(actionPanel);
        mainPanel.add(statusPanel);

        bl = new BoxLayout(mainPanel, BoxLayout.Y_AXIS);
        mainPanel.setLayout(bl);

        frame.getContentPane().add(mainPanel);
        // frame.setPreferredSize(new Dimension(800,600));
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String args[]) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new DDLCheckerGUI().createAndShowGUI();
            }
        });
    }

    public void setDelMgr(DeliciousPostsMgrV1 delMgr) {
        this.delMgr = delMgr;
    }

    public DeliciousPostsMgrV1 getDelMgr() {
        return delMgr;
    }
}

class SwingUtil {
    public static String cvtNewline2BR(String str) {
        if (str == null)
            return null;
        return str.replaceAll("\n", "<br>");
    }
}

/**
 * This listener listens to actions invoked via GUI.
 * 
 * @author zhguo
 * 
 */
class DDLCheckerGUIListener {
    /* GUI this listener corresponds to */
    private DDLCheckerGUI gui;
    private LoginHandler  loginHanlder = new LoginHandler();

    public DDLCheckerGUIListener(DDLCheckerGUI gui) {
        this.setGui(gui);
    }

    public void loginComplete(boolean succ, String msg) {
        loginHanlder.loginComplete(getGui(), succ, msg);
    }

    public void loginStarted() {
        loginHanlder.loginStarted(getGui());
    }

    public void setGui(DDLCheckerGUI gui) {
        this.gui = gui;
    }

    public DDLCheckerGUI getGui() {
        return gui;
    }
}

class LoginHandler {
    private boolean started  = false;
    private boolean finished = false;

    public void loginComplete(DDLCheckerGUI gui, boolean succ, String msg) {
        if (!started)
            System.err.println("Something abnormal happened.");
        JLabel statusLabel = (JLabel) gui.getComponent("status-label");
        if (succ)
            statusLabel.setText("Login succeeded! "
                    + SwingUtil.cvtNewline2BR(msg));
        else
            statusLabel
                    .setText("Login failed! " + SwingUtil.cvtNewline2BR(msg));
        finished = true;
        started = false;
    }

    public void loginStarted(DDLCheckerGUI gui) {
        if (started && !finished)
            return;
        JLabel statusLabel = (JLabel) gui.getComponent("status-label");
        statusLabel.setText("Login started! ");
        started = true;
        finished = false;
    }
}

class ActionListenerBase implements ActionListener {
    protected DDLCheckerGUIListener guiListener;

    @Override
    public void actionPerformed(ActionEvent e) {
    }

    public ActionListenerBase(DDLCheckerGUIListener guiListener) {
        this.guiListener = guiListener;
    }
}

class BookmarksLoadRecentBtListener extends ActionListenerBase {
    public BookmarksLoadRecentBtListener(DDLCheckerGUIListener guiListener) {
        super(guiListener);
    }

    @Override
    public void actionPerformed(ActionEvent evt) {
        if (!evt.getActionCommand().equals("load-recent-bookmarks"))
            return;
        if (guiListener == null)
            return;
        
        JProgressBar progressBar = (JProgressBar)guiListener.getGui().getComponent("progress-bar");
        progressBar.setValue(0);
        
        try {
            boolean fromCache = true;
            Collection<Link> links = null;
            if (!guiListener.getGui().getCacheMgr().isDisabled()) {
                links = guiListener.getGui().getCacheMgr().getRecentPosts();
            } 
            if (links == null) {
                fromCache = false;
                String resp = guiListener.getGui().getDelMgr().getRecentPosts();
                links = guiListener.getGui().getDelMgr().cvtPosts2Link(resp);
            }
            
            JTable linksTb = (JTable) guiListener.getGui().getComponent(
                    "links-table");
            ((LinksTableModel) linksTb.getModel()).setLinks(links);
            
            JTable validLinksTb = (JTable) guiListener.getGui().getComponent(
            "valid-links-table");
            ((LinksTableModel) validLinksTb.getModel()).setLinks(null);
            
            JTable invalidLinksTb = (JTable) guiListener.getGui().getComponent(
            "invalid-links-table");
            ((LinksTableModel) invalidLinksTb.getModel()).setLinks(null);
            
            if (!fromCache)
                guiListener.getGui().getCacheMgr().setRecentPosts(links);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        }
    }
}

class BookmarksLoadAllBtListener extends ActionListenerBase {
    public BookmarksLoadAllBtListener(DDLCheckerGUIListener guiListener) {
        super(guiListener);
    }

    @Override
    public void actionPerformed(ActionEvent evt) {
        if (!evt.getActionCommand().equals("load-all-bookmarks"))
            return;
        if (guiListener == null)
            return;
        try {
            boolean fromCache = true;
            Collection<Link> links = null;
            if (!guiListener.getGui().getCacheMgr().isDisabled()) {
                links = guiListener.getGui().getCacheMgr().getAllPosts();
            } 
            if (links == null) {
                fromCache = false;
                String resp = guiListener.getGui().getDelMgr().getAllPosts();
                links = guiListener.getGui().getDelMgr().cvtPosts2Link(resp);
            }
            JTable linksTb = (JTable) guiListener.getGui().getComponent(
                    "links-table");
            ((LinksTableModel) linksTb.getModel()).setLinks(links);
            
            JTable validLinksTb = (JTable) guiListener.getGui().getComponent(
            "valid-links-table");
            ((LinksTableModel) validLinksTb.getModel()).setLinks(null);
            
            JTable invalidLinksTb = (JTable) guiListener.getGui().getComponent(
            "invalid-links-table");
            ((LinksTableModel) invalidLinksTb.getModel()).setLinks(null);
            
            if (!fromCache)
                guiListener.getGui().getCacheMgr().setAllPosts(links);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        }
    }
}

class LoginBtActionListener extends ActionListenerBase {
    private JTextField     usernameField;
    private JPasswordField passwordField;

    public LoginBtActionListener(DDLCheckerGUIListener guiListener) {
        super(guiListener);
        this.usernameField = (JTextField) guiListener.getGui().getComponent(
                "username-textfield");
        this.passwordField = (JPasswordField) guiListener.getGui()
                .getComponent("password-textfield");
    }

    private void retryUI() {
        if (this.usernameField == null)
            this.usernameField = (JTextField) guiListener.getGui()
                    .getComponent("username-textfield");
        if (this.passwordField == null)
            this.passwordField = (JPasswordField) guiListener.getGui()
                    .getComponent("password-textfield");
    }

    @Override
    public void actionPerformed(ActionEvent e) {
            retryUI();

            String username = usernameField.getText();
            String password = new String(passwordField.getPassword());
            guiListener.getGui().getDelMgr().setUsername(username);
            guiListener.getGui().getDelMgr().setPassword(password);

            if (guiListener != null)
                guiListener.loginStarted();

            if (guiListener != null) {
                StringBuilder outputMsg = new StringBuilder();
                boolean succ = guiListener.getGui().getDelMgr().login(outputMsg);
                guiListener.loginComplete(succ, outputMsg.toString());
            }

    }
}
/*
class LoginBtActionListener extends ActionListenerBase {
    private JTextField     usernameField;
    private JPasswordField passwordField;

    public LoginBtActionListener(DDLCheckerGUIListener guiListener) {
        super(guiListener);
        this.usernameField = (JTextField) guiListener.getGui().getComponent(
                "username-textfield");
        this.passwordField = (JPasswordField) guiListener.getGui()
                .getComponent("password-textfield");
    }

    private void retryUI() {
        if (this.usernameField == null)
            this.usernameField = (JTextField) guiListener.getGui()
                    .getComponent("username-textfield");
        if (this.passwordField == null)
            this.passwordField = (JPasswordField) guiListener.getGui()
                    .getComponent("password-textfield");
    }

    @Override
    public void actionPerformed(ActionEvent e) {
            retryUI();

            String username = usernameField.getText();
            String password = new String(passwordField.getPassword());
            guiListener.getGui().getDelMgr().setUsername(username);
            guiListener.getGui().getDelMgr().setPassword(password);

            if (guiListener != null)
                guiListener.loginStarted();

            if (guiListener != null) {
                StringBuilder outputMsg = new StringBuilder();
                boolean succ = guiListener.getGui().getDelMgr().login(outputMsg);
                guiListener.loginComplete(succ, outputMsg.toString());
            }

    }
}
*/

class LinksTableModel extends AbstractTableModel {
    private static final long serialVersionUID = 1L;

    
    /* 0 - 9 are reserved for hidden columns */
    public static final int   HIDDEN_COLUMN_COUNT           = 10;
    public static final int   LINK_CHECKING_RESULT_INDEX    = 0;
    
    public static final int   URL_INDEX        = HIDDEN_COLUMN_COUNT;
    public static final int   DESP_INDEX       = HIDDEN_COLUMN_COUNT+1;
    public static final int   ADDTIME_INDEX    = HIDDEN_COLUMN_COUNT+2;
    public static final int   NOTE_INDEX       = HIDDEN_COLUMN_COUNT+3;
    public static final int   HIDDEN_INDEX     = HIDDEN_COLUMN_COUNT+4;

    final String columnNames[]    = { "URL", "Description", "Addition time", "Note"};
    
    private List<Link>        links;

    public LinksTableModel() {
        links = new ArrayList<Link>();
    }

    public LinksTableModel(Collection<Link> links) {
        this.setLinks(links);
    }

    public String getColumnName(int column) {
        return columnNames[column];
    }

    public boolean isCellEditable(int row, int column) {
        if (column + HIDDEN_COLUMN_COUNT == HIDDEN_INDEX)
            return false;
        else
            return true;
    }

    public Class getColumnClass(int column) {
        column += HIDDEN_COLUMN_COUNT;
        switch (column) {
        case URL_INDEX:
        case DESP_INDEX:
            return String.class;
        case ADDTIME_INDEX:
            return Date.class;
        case NOTE_INDEX:
            return String.class;
        default:
            return Object.class;
        }
    }

    public Object getValueAt(int row, int column) {
        column += HIDDEN_COLUMN_COUNT;
        Link link = getLinks().get(row);
        switch (column) {
        case URL_INDEX:
            return link.getUrl().toString();
        case DESP_INDEX:
            return link.getProperty("description");
        case ADDTIME_INDEX:
            return link.getProperty("addTime");
        case NOTE_INDEX:
            LinkCheckResult lcr = (LinkCheckResult)getValueAtRaw(row, LINK_CHECKING_RESULT_INDEX);
            if (lcr != null) {
                return lcr.getResult().getDescription();
            } else return "";
        default:
            return new Object();
        }
    }

    public Object getValueAtRaw(int row, int column) {
        if (column == LINK_CHECKING_RESULT_INDEX) {
            String url = (String)getValueAt(row, URL_INDEX-HIDDEN_COLUMN_COUNT);
            return resultMap.get(url);
        }
        return null;
    }
    
    private Map<String, LinkCheckResult> resultMap = 
        new HashMap<String, LinkCheckResult>();
    
    public void addLinkCheckResult(LinkCheckResult result) {
        resultMap.put(result.getLinkChecked().getUrl().toString(), result);
        Object obj = linksToIndexMap.get(result.getLinkChecked().getUrl().toString());
        if (obj != null) {
            int idx = (Integer)obj;
            fireTableRowsUpdated(idx, idx);
        } else {
            this.linksToIndexMap.put(result.getLinkChecked().getUrl().toString(), this.links.size());
            this.links.add(result.getLinkChecked());
            fireTableRowsUpdated(this.links.size()-1, this.links.size()-1);
        }
    }
    
    public void setValueAt(Object value, int row, int column) {
        Link link = getLinks().get(row);
        column += HIDDEN_COLUMN_COUNT;
        switch (column) {
        case URL_INDEX:
            try {
                link.setUrl(new URL((String) value));
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
            break;
        case DESP_INDEX:
            link.setProperty("description", (String) value);
            break;
        case ADDTIME_INDEX:
            // link.setProperty("addTime", );
            break;
        case NOTE_INDEX:
            break;
        default:
            System.out.println("invalid index");
        }
        fireTableCellUpdated(row, column);
    }

    public int getRowCount() {
        return getLinks().size();
    }

    public int getColumnCount() {
        return columnNames.length;
    }

    private Map<String, Integer>linksToIndexMap = new HashMap<String, Integer>();
    
    public void clear() {
        if (this.links != null) {
            this.links.clear();
            this.linksToIndexMap.clear();
            this.resultMap.clear();
        }
    }
    public void setLinks(Collection<Link> links) {
        clear();
        if (links == null) {
            this.fireTableDataChanged();
            return;
        }
        
        this.links = new ArrayList<Link>(links);
        for (int i = 0; i < this.links.size(); ++i) {
            Link link = this.links.get(i);
            this.linksToIndexMap.put(link.getUrl().toString(), i);
        }
        this.fireTableDataChanged();
    }

    public List<Link> getLinks() {
        return links;
    }

    /*
     * public boolean hasEmptyRow() { if (dataVector.size() == 0) return false;
     * AudioRecord audioRecord = (AudioRecord)dataVector.get(dataVector.size() -
     * 1); if (audioRecord.getTitle().trim().equals("") &&
     * audioRecord.getArtist().trim().equals("") &&
     * audioRecord.getAlbum().trim().equals("")) { return true; } else return
     * false; }
     * 
     * public void addEmptyRow() { dataVector.add(new AudioRecord());
     * fireTableRowsInserted( dataVector.size() - 1, dataVector.size() - 1); }
     */
}

class GUIResultIndictorConstant {
    public static Color failed = Color.RED; 
    public static Color completed = new Color(0, 200, 0); 
}

class LinkJLabelCR extends DefaultTableCellRenderer  {

    private static final long serialVersionUID = 1L;

    public LinkJLabelCR() { }

    public Component getTableCellRendererComponent(JTable table, Object value,
            boolean isSelected, boolean hasFocus, int row, int column) {
        super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
        
        LinksTableModel tblModel = (LinksTableModel)table.getModel();
        LinkCheckResult result = 
            (LinkCheckResult)tblModel.getValueAtRaw(row, LinksTableModel.LINK_CHECKING_RESULT_INDEX);
        
        if (result != null) {
            if (result.getResult() == ResultCode.FAILED)
                this.setBackground(GUIResultIndictorConstant.failed);
            else
                setBackground(GUIResultIndictorConstant.completed);
        } else
            setBackground(Color.WHITE);
        
        return this;
    }
}

class GUILinkCheckListener implements LinkCheckListener {
    private DDLCheckerGUI gui;

    public GUILinkCheckListener(DDLCheckerGUI gui) {
        this.gui = gui;
    }
    
    @Override
    public void linkCheckAllComplete() {
        JLabel statusLabel = (JLabel) gui.getComponent("status-label");
        statusLabel.setText("Link checking completed");
    }

    @Override
    public void linkCheckStart() {
        JLabel statusLabel = (JLabel) gui.getComponent("status-label");
        statusLabel.setText("Link checking started");
        
        JProgressBar progressBar = (JProgressBar) gui.getComponent("progress-bar");
        JTable tbl = (JTable)gui.getComponent("links-table");
        LinksTableModel model = (LinksTableModel)tbl.getModel();
        progressBar.setMaximum(model.getLinks().size());
        progressBar.setValue(0);
    }

    @Override
    public synchronized void singleLinkCheckComplete(LinkCheckResult result) {
        JTable linkTbl = (JTable)gui.getComponent("links-table");
        LinksTableModel tblModel = (LinksTableModel)linkTbl.getModel();
        tblModel.addLinkCheckResult(result);
        
        if (result.getResult() != ResultCode.SUCCEED) {
            JTable linkInvalidTbl = (JTable)gui.getComponent("invalid-links-table");
            LinksTableModel invalidTblModel = (LinksTableModel)linkInvalidTbl.getModel();
            invalidTblModel.addLinkCheckResult(result);
        } else {
            JTable validTbl = (JTable)gui.getComponent("valid-links-table");
            LinksTableModel validTblModel = (LinksTableModel)validTbl.getModel();
            validTblModel.addLinkCheckResult(result);
        }
        JProgressBar progressBar = (JProgressBar) gui.getComponent("progress-bar");
        progressBar.setValue(progressBar.getValue()+1);
    }
}

class BareBonesBrowserLaunch {

    static final String[] browsers = { "google-chrome", "firefox", "opera",
            "konqueror", "epiphany", "seamonkey", "galeon", "kazehakase",
            "mozilla"             };
    static final String   errMsg   = "Error attempting to launch web browser";

    /**
     * Opens the specified web page in the user's default browser
     * 
     * @param url
     *            A web address (URL) of a web page (ex:
     *            "http://www.google.com/")
     */
    public static void openURL(String url) {
        try { // attempt to use Desktop library from JDK 1.6+ (even if on 1.5)
            Class<?> d = Class.forName("java.awt.Desktop");
            d.getDeclaredMethod("browse", new Class[] { java.net.URI.class })
                    .invoke(d.getDeclaredMethod("getDesktop").invoke(null),
                            new Object[] { java.net.URI.create(url) });
            // above code mimics:
            // java.awt.Desktop.getDesktop().browse(java.net.URI.create(url));
        } catch (Exception ignore) { // library not available or failed
            String osName = System.getProperty("os.name");
            try {
                if (osName.startsWith("Mac OS")) {
                    Class.forName("com.apple.eio.FileManager")
                            .getDeclaredMethod("openURL",
                                    new Class[] { String.class }).invoke(null,
                                    new Object[] { url });
                } else if (osName.startsWith("Windows"))
                    Runtime.getRuntime().exec(
                            "rundll32 url.dll,FileProtocolHandler " + url);
                else { // assume Unix or Linux
                    boolean found = false;
                    for (String browser : browsers)
                        if (!found) {
                            found = Runtime.getRuntime().exec(
                                    new String[] { "which", browser })
                                    .waitFor() == 0;
                            if (found)
                                Runtime.getRuntime().exec(
                                        new String[] { browser, url });
                        }
                    if (!found)
                        throw new Exception(Arrays.toString(browsers));
                }
            } catch (Exception e) {
                JOptionPane.showMessageDialog(null, errMsg + "\n"
                        + e.toString());
            }
        }
    }
}
